package datasource

import (
	"net/http"
	"strconv"
	"time"

	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-incubator/tidb-dashboard/pkg/ldap"

	"github.com/gin-gonic/gin"
	"github.com/joomcode/errorx"
	"go.uber.org/fx"

	"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/user"
	"github.com/pingcap-incubator/tidb-dashboard/pkg/apiserver/utils"
	"github.com/pingcap-incubator/tidb-dashboard/pkg/config"
	"github.com/pingcap-incubator/tidb-dashboard/pkg/model"
	"github.com/pingcap-incubator/tidb-dashboard/pkg/repo"
	"github.com/pingcap-incubator/tidb-dashboard/pkg/tidb"
)

var (
	ErrNS               = errorx.NewNamespace("error.api.datasource")
	ErrNSInstance       = ErrNS.NewSubNamespace("instance")
	ErrInstanceBizErr   = ErrNSInstance.NewType("biz_err")
	ErrInstanceParamErr = ErrNSInstance.NewType("invalid_param")
)

type ServiceParams struct {
	fx.In
	TiDBClient        *tidb.Client
	TiDBClientFactory *tidb.Factory
	Config            *config.Config
	LDAPClient        *ldap.Client
}

type Service struct {
	params ServiceParams
}

func NewService(p ServiceParams) *Service {
	return &Service{params: p}
}

func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) {
	endpoint := r.Group("/datasource")

	endpoint.Use(auth.MWAuthRequired())
	endpoint.Use(utils.MWConnectTiDBDefault(s.params.TiDBClient))

	endpoint.GET("/instances", s.listInstancesHandler)
	endpoint.GET("/instance", s.getInstanceHandler)
	endpoint.POST("/instance", s.createInstanceHandler)
	endpoint.DELETE("/instance", s.deleteInstanceHandler)
	endpoint.PUT("/instance", s.updateInstanceHandler)
	endpoint.PUT("/instance/status", s.updateInstanceStatusHandler)

	endpoint.GET("permission", s.getPermissionHandler)
	endpoint.DELETE("permission", s.deletePermissionHandler)
	endpoint.POST("permission", s.createPermissionHandler)

	endpoint.GET("/test", s.testConnectionHandler)
}

type InstanceVo struct {
	ID       uint   `json:"id"`
	Name     string `json:"name"`     //实例名称
	Address  string `json:"address"`  //地址
	Username string `json:"username"` //用户名
	Password string `json:"password"` //密码
	Index    uint8  `json:"index"`    //显示顺序（升序）
	Status   uint8  `json:"status"`   //是否启用
}

// @ID createInstance
// @Summary create instance
// @Param request body InstanceVo true "Request body"
// @Security JwtAuth
// @Success 200 "success"
// @Router /datasource/instance [post]
func (s *Service) createInstanceHandler(c *gin.Context) {
	var req InstanceVo
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.MakeInvalidRequestErrorFromError(c, err)
		return
	}
	ts := time.Now()
	instance := model.TidbInstance{
		Name:       req.Name,
		Address:    req.Address,
		Username:   req.Username,
		Password:   req.Password,
		Index:      req.Index,
		Status:     1,
		CreateTime: &ts,
		UpdateTime: &ts,
	}
	db := utils.GetTiDBConnection(c)

	err := tidb.EncryptInstanceParam(&instance, s.params.Config.DatasourceKey)
	if err != nil {
		_ = c.Error(err)
		return
	}

	err = repo.AddInstance(db, &instance)
	if err != nil {
		_ = c.Error(err)
		return
	}
	c.String(http.StatusOK, "success")
}

// @ID updateInstance
// @Summary update instance
// @Param request body InstanceVo true "Request body"
// @Security JwtAuth
// @Success 200 "success"
// @Router /datasource/instance [put]
func (s *Service) updateInstanceHandler(c *gin.Context) {
	var req InstanceVo
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.MakeInvalidRequestErrorFromError(c, err)
		return
	}
	instance := model.TidbInstance{
		Name:     req.Name,
		Address:  req.Address,
		Username: req.Username,
		Password: req.Password,
		Index:    req.Index,
		Status:   req.Status,
	}
	db := utils.GetTiDBConnection(c)

	err := tidb.EncryptInstanceParam(&instance, s.params.Config.DatasourceKey)
	if err != nil {
		_ = c.Error(err)
		return
	}

	err = repo.UpdateInstance(db, &instance, req.ID)
	if err != nil {
		_ = c.Error(err)
		return
	}
	c.String(http.StatusOK, "success")
}

type UpstateInstanceStatusRequest struct {
	ID     uint  `json:"id"`
	Status uint8 `json:"status"` //是否启用
}

// @ID updateInstanceStatus
// @Summary update instance status
// @Param request body UpstateInstanceStatusRequest true "Request body"
// @Security JwtAuth
// @Success 200 "success"
// @Router /datasource/instance/status [put]
func (s *Service) updateInstanceStatusHandler(c *gin.Context) {
	var req UpstateInstanceStatusRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.MakeInvalidRequestErrorFromError(c, err)
		return
	}

	db := utils.GetTiDBConnection(c)

	err := repo.UpdateInstanceStatus(db, req.ID, req.Status)
	if err != nil {
		_ = c.Error(err)
		return
	}
	c.String(http.StatusOK, "success")
}

type ListInstanceResponse struct {
	Instances []InstanceVo `json:"instances"`
}

// @ID listInstances
// @Summary get instance list
// @Security JwtAuth
// @Success 200 {object} ListInstanceResponse
// @Router /datasource/instances [get]
func (s *Service) listInstancesHandler(c *gin.Context) {
	db := utils.GetTiDBConnection(c)
	instances, err := repo.ListAllInstance(db)
	if err != nil {
		_ = c.Error(err)
		return
	}
	instanceList := make([]InstanceVo, 0, len(instances))
	for i := range instances {
		instance := instances[i]
		err := tidb.DecryptInstanceParam(&instance, s.params.Config.DatasourceKey)
		if err != nil {
			_ = c.Error(err)
			return
		}
		instanceVo := InstanceVo{
			ID:       instance.ID,
			Name:     instance.Name,
			Address:  instance.Address,
			Username: instance.Username,
			Password: instance.Password,
			Index:    instance.Index,
			Status:   instance.Status,
		}
		instanceList = append(instanceList, instanceVo)
	}
	c.JSON(http.StatusOK, &ListInstanceResponse{Instances: instanceList})
}

// @ID getInstance
// @Param id query int true "id"
// @Summary get instance
// @Security JwtAuth
// @Success 200 {object} InstanceVo
// @Router /datasource/instance [get]
func (s *Service) getInstanceHandler(c *gin.Context) {
	id, ok := c.GetQuery("id")

	if !ok {
		_ = c.Error(ErrInstanceParamErr.New("missing param id"))
		return
	}

	db := utils.GetTiDBConnection(c)

	instanceID, err := strconv.Atoi(id)
	if err != nil {
		_ = c.Error(err)
		return
	}

	instance, err := repo.GetInstance(db, uint(instanceID))
	if err != nil {
		_ = c.Error(err)
		return
	}
	if instance == nil {
		_ = c.Error(ErrInstanceBizErr.New("missing param id"))
		return
	}

	err = tidb.DecryptInstanceParam(instance, s.params.Config.DatasourceKey)
	if err != nil {
		_ = c.Error(err)
		return
	}

	instanceVo := InstanceVo{
		ID:       instance.ID,
		Name:     instance.Name,
		Address:  instance.Address,
		Username: instance.Username,
		Password: instance.Password,
		Index:    instance.Index,
		Status:   instance.Status,
	}
	c.JSON(http.StatusOK, &instanceVo)
}

type DeleteInstanceRequest struct {
	ID uint `json:"id"`
}

// @ID deleteInstance
// @Summary delete instance by id
// @Param request body DeleteInstanceRequest true "Request body"
// @Security JwtAuth
// @Success 200 "success"
// @Router /datasource/instance [delete]
func (s *Service) deleteInstanceHandler(c *gin.Context) {
	var req DeleteInstanceRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.MakeInvalidRequestErrorFromError(c, err)
		return
	}
	db := utils.GetTiDBConnection(c)

	err := repo.DeleteInstancePermissionByInstanceID(db, req.ID)
	if err != nil {
		_ = c.Error(err)
		return
	}

	err = repo.DeleteInstance(db, req.ID)
	if err != nil {
		_ = c.Error(err)
		return
	}
	c.String(http.StatusOK, "success")
}

type TestConnectionResponse struct {
	Success bool `json:"success"`
}

// @ID testConnection
// @Param id query int true "id"
// @Summary get instance
// @Security JwtAuth
// @Success 200 {object} TestConnectionResponse
// @Router /datasource/test [get]
func (s *Service) testConnectionHandler(c *gin.Context) {
	id, ok := c.GetQuery("id")

	if !ok {
		_ = c.Error(ErrInstanceParamErr.New("missing param id"))
		return
	}

	db := utils.GetTiDBConnection(c)

	instanceID, err := strconv.Atoi(id)
	if err != nil {
		_ = c.Error(err)
		return
	}

	instance, err := repo.GetInstance(db, uint(instanceID))
	if err != nil {
		_ = c.Error(err)
		return
	}
	if instance == nil {
		_ = c.Error(ErrInstanceBizErr.New("missing param id"))
		return
	}

	err = tidb.DecryptInstanceParam(instance, s.params.Config.DatasourceKey)
	if err != nil {
		_ = c.Error(err)
		return
	}

	client := &tidb.Client{
		SQLAPIAddress:   instance.Address,
		DefaultUsername: instance.Username,
		DefaultPassword: instance.Password,
		UpdateTime:      instance.UpdateTime.Unix(),
	}
	_, err = client.OpenSQLConn(instance.Username, instance.Password)
	if err != nil {
		c.JSON(http.StatusOK, &TestConnectionResponse{Success: false})
		return
	}
	c.JSON(http.StatusOK, &TestConnectionResponse{Success: true})
}

type GetPermissionResponse struct {
	Permissions []model.GetUserPermission `json:"permissions"`
}

// @ID getPermission
// @Summary get instance permission list
// @Param instance_id query string true "instance ID"
// @Security JwtAuth
// @Success 200 {object} GetPermissionResponse
// @Router /datasource/permission [get]
func (s *Service) getPermissionHandler(c *gin.Context) {
	val, ok := c.GetQuery("instance_id")
	if !ok {
		_ = c.Error(ErrInstanceParamErr.New("invalid param instance_id"))
		return
	}
	instanceID, err := strconv.Atoi(val)
	if err != nil {
		_ = c.Error(err)
		return
	}

	db := utils.GetTiDBConnection(c)
	result, err := repo.GetUserPermissionByInstanceID(db, uint(instanceID))
	if err != nil {
		_ = c.Error(err)
		return
	}
	c.JSON(http.StatusOK, &GetPermissionResponse{Permissions: result})
}

type DeletePermissionRequest struct {
	ID uint `form:"id"`
}

// @ID deletePermission
// @Summary delete instance permission
// @Param id query string true "ID"
// @Security JwtAuth
// @Success 200 "success"
// @Router /datasource/permission [delete]
func (s *Service) deletePermissionHandler(c *gin.Context) {
	val, ok := c.GetQuery("id")
	if !ok {
		_ = c.Error(ErrInstanceParamErr.New("id"))
		return
	}
	ID, err := strconv.Atoi(val)
	if err != nil {
		_ = c.Error(err)
		return
	}
	db := utils.GetTiDBConnection(c)
	err = repo.DeleteInstancePermissionByID(db, uint(ID))
	if err != nil {
		_ = c.Error(err)
		return
	}
	c.String(http.StatusOK, "success")
}

type CreatePermissionRequest struct {
	InstanceID uint     `json:"instance_id"`
	UserID     []string `json:"user_id"`
}

// @ID createPermission
// @Summary get instance permission list
// @Param request body CreatePermissionRequest true "Request body"
// @Security JwtAuth
// @Success 200 "success"
// @Router /datasource/permission [post]
func (s *Service) createPermissionHandler(c *gin.Context) {
	var req CreatePermissionRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		utils.MakeInvalidRequestErrorFromError(c, err)
		return
	}

	sessionUser := c.MustGet(utils.SessionUserKey).(*utils.SessionUser)

	if sessionUser.IsSQLUser || sessionUser.IsShared {
		_ = c.Error(ErrInstanceBizErr.New("请通过丰巢帐号登录操作"))
		c.Status(http.StatusBadRequest)
		return
	}

	instanceID := req.InstanceID
	userIDs := req.UserID

	db := utils.GetTiDBConnection(c)
	instance, err := repo.GetInstance(db, instanceID)
	if err != nil {
		_ = c.Error(err)
		return
	}
	if instance == nil {
		_ = c.Error(ErrInstanceBizErr.New("instance not found"))
		return
	}

	existUser := make([]string, 0)

	for _, userID := range userIDs {
		userInfo, err := repo.GetUser(db, userID)
		if err != nil {
			_ = c.Error(err)
			return
		}
		if userInfo != nil {
			existUser = append(existUser, userID)
		} else {
			createErr := s.createUserByUserID(c, sessionUser.Username, sessionUser.Password, userID)
			if createErr == nil {
				existUser = append(existUser, userID)
			} else {
				log.Error("Create user err.userId="+userID, zap.Error(err))
			}
		}
	}

	for _, userID := range existUser {
		err = repo.AddInstancePermission(db, instanceID, userID)
		if err != nil {
			_ = c.Error(err)
			return
		}
	}
	c.String(http.StatusOK, "success")
}

func (s *Service) createUserByUserID(c *gin.Context, userName string, password, targetUserID string) error {
	ldapUser, err := s.params.LDAPClient.QueryUserInfoByUserID(userName, password, targetUserID)

	if err != nil {
		return err
	}

	if ldapUser == nil {
		return ErrInstanceBizErr.New("未查询到该用户")
	}

	t := time.Now()
	user := model.User{
		UserID:      ldapUser.ID,
		Name:        ldapUser.Name,
		Phone:       ldapUser.Phone,
		Mail:        ldapUser.Mail,
		Description: ldapUser.Description,
		CreateTime:  &t,
	}
	db := utils.GetTiDBConnection(c)

	err = repo.CreateUser(db, &user)
	if err != nil {
		return err
	}

	return nil
}
